package org.wwb.antbox.center.web.shiro;

import com.alibaba.fastjson.JSON;
import org.ant.commons.commons.bean.BeanTool;
import org.ant.commons.commons.context.CommonContext;
import org.ant.commons.commons.context.ContextParam;
import org.ant.commons.commons.date.DateTool;
import org.ant.commons.commons.log.Log;
import org.ant.commons.commons.log.LogFactory;
import org.ant.commons.commons.login.LoginResult;
import org.ant.commons.models.model.BaseIdModel;
import org.ant.commons.models.result.BaseResult;
import org.ant.model.common.session.SessionUser;
import org.ant.shiro.jwt.JwtToken;
import org.ant.shiro.jwt.JwtUtil;
import org.ant.shiro.jwt.TokenManagerShiro;
import org.ant.shiro.session.SessionManagerShiro;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.wwb.antbox.user.iservice.ServiceUserTool;
import org.wwb.antbox.user.model.common.exception.AccountDisabledException;
import org.wwb.antbox.user.model.common.exception.AccountInActiveException;
import org.wwb.antbox.user.model.common.exception.AccountPasswordException;
import org.wwb.antbox.user.model.passport.view.RoleNamesAndPermissionsView;
import org.wwb.antbox.user.model.user.entity.SysUser;
import org.wwb.antbox.user.model.user.enums.SysUserStatusEnum;
import org.wwb.antbox.user.model.user.model.SysUserFreezeModel;
import org.wwb.antbox.user.model.user.model.SysUserLoginModel;
import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.LoginException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Set;

import static org.ant.model.common.session.SessionKey.S_LOGIN_ERROR_TIMES;

/**
 * @Author Bond
 * @Date: 2020/3/3 16:27
 * @Description: 登录过滤器
 */
public class LocalLoginFilter extends FormAuthenticationFilter {

    public static final String BEAN_NAME = "localLoginFilter";

    private static final Log log = LogFactory.getLog(LocalLoginFilter.class);

    /**
     * 登录出错5次就锁定用户
     */
    private static final Integer AUTHENTICATION_FAILURE_FREEZE_LIMIT = 5;

    /**
     * 冻结小时
     */
    private static final Integer AUTHENTICATION_FAILURE_FREEZE_HOUR = 3;

    /**
     * 验证码 参数变量名称
     */
    private static final String CAPTCHA_PARAM = "captcha";
    /**
     * 动态验证码 参数变量名称
     */
    private static final String AUTHENTICATION_PARAM = "authentication";

    /**
     * 登录账号 参数变量名称
     */
    private static final String LOGIN_NAME_PARAM = "loginName";

    /**
     * 登录密码 参数变量名称
     */
    private static final String LOGIN_PWD_PARAM = "loginPwd";

    /**
     * Token 刷新时间
     */
    private String refreshTokenExpireTime;

    /**
     * Token管理
     */
    private TokenManagerShiro tokenManagerShiro;

    public void setRefreshTokenExpireTime(String refreshTokenExpireTime) {
        this.refreshTokenExpireTime = refreshTokenExpireTime;
    }

    public void setTokenManagerShiro(TokenManagerShiro tokenManagerShiro) {
        this.tokenManagerShiro = tokenManagerShiro;
    }

    /**
     * 是否允许访问
     * @param servletRequest
     * @param servletResponse
     * @param mappedValue
     * @return 允许返回 true ，需要登录则返回 false
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) {
        Subject subject = SecurityUtils.getSubject();
        // 判断是否已经登录成功
        if (!subject.isAuthenticated()) {
            // 用户还没登录继续进行
            return false;
        }
        log.info("用户【{0}】已经登录过了，直接访问!",subject.getPrincipal());
        return true;
    }

    /**
     * 表示当访问拒绝时是否已经处理了
     * 如果isAccessAllowed返回false，则系统执行onAccessDenied
     * @param servletRequest
     * @param servletResponse
     * @param mappedValue
     * @return 如果返回true表示需要继续处理；如果返回false表示该拦截器实例已经处理了，将直接返回即可
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception {
        // 判断请求是否是登录请求
        if(isLoginRequest(servletRequest, servletResponse)){
            // 判断请求是否是post方法
            if (isLoginSubmission(servletRequest, servletResponse)) {
                // 执行登录验证
                return executeLogin(servletRequest, servletResponse);
            }else {
                // 如果是get方法则会返回true,跳转到登陆页面
                log.warn("用户请求方式是非【POST】请求，跳转到登录页!");
                return true;
            }
        }else {
            log.warn("用户未登录/访问非登录界面，跳转到登录页!");
            // 如果访问的是非登录页面，则跳转到登录页面
            redirectToLogin(servletRequest, servletResponse);
            return false;
        }
    }

    /**
     * 执行真正的登录操作
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        setUsernameParam(LOGIN_NAME_PARAM);
        setPasswordParam(LOGIN_PWD_PARAM);

        // 用户账号
        String loginName = getUsername(request);
        // 密码
        String loginPwd = getPassword(request);
        // 验证码
        String captcha = getCaptch(request);
        // 动态验证码
        String authentication = getAuthentication(request);


        LoginResult loginResult = new LoginResult();
        loginResult.setSuccess(true);
        loginResult.setOpenCaptcha(false);
        loginResult.setUserName(loginName);
        loginResult.setPassWord(loginPwd);

        try {
            // 校验用户名
            if(StringUtils.isBlank(loginName)){
                loginResult.setSuccess(false);
                loginResult.getPropMessages().putIfAbsent(getUsernameParam(),"用户名不能为空");
                outPutLoginResult(response, loginResult);
                return false;
            }
            // 校验密码
            if(StringUtils.isBlank(loginPwd)){
                loginResult.setSuccess(false);
                loginResult.getPropMessages().putIfAbsent(getPasswordParam(),"密码不能为空");
                outPutLoginResult(response, loginResult);
                return false;
            }

            Subject subject = SecurityUtils.getSubject();


            // ###### 1、从数据库根据用户名和密码 匹配系统用户对象 start #######
            SysUserLoginModel sysUserLoginModel = new SysUserLoginModel();
            sysUserLoginModel.setLoginName(loginName);
            sysUserLoginModel.setLoginPwd(loginPwd);

            BaseResult<SysUser> checkSysUserResult = ServiceUserTool.passportService().checkLoginSysUser(sysUserLoginModel);
            // ###### 1、从数据库根据用户名和密码 匹配系统用户对象 end   #######

            SysUser sysUser = checkSysUserResult.getModel();

            // ###### 2、根据用户Id获取用户拥有的角色名称集合和权限code集合  并存入Session 中 start #######
            BaseIdModel<String> userIdModel = new BaseIdModel<>(sysUser.getId());

            BaseResult<RoleNamesAndPermissionsView> userRoleNamesAndPermissions = ServiceUserTool.passportService().getUserRoleNamesAndPermissions(userIdModel);
            RoleNamesAndPermissionsView roleNamesAndPermissionsView = userRoleNamesAndPermissions.getModel();

            // 用户角色名称集合
            Set<String> userRoleNames = roleNamesAndPermissionsView.getUserRoleNames();

            // 获取用户权限集合
            Set<String> allPermissions = roleNamesAndPermissionsView.getAllPermissions();

            // 设置RefreshToken，时间戳为当前时间戳，直接设置即可(不用先删后设，会覆盖已有的RefreshToken)
            String currentTimeMillis = String.valueOf(System.currentTimeMillis());

            // 从Header中Authorization返回AccessToken，时间戳为当前时间戳
            String token = JwtUtil.sign(loginName, currentTimeMillis);

            // 初始化SessionUser信息
            SessionUser sessionUser = setSessionUser(sysUser);
            sessionUser.setToken(token);
            sessionUser.setRoleNames(userRoleNames);
            sessionUser.setPermissions(allPermissions);

            saveToRedis(currentTimeMillis,loginName,sessionUser);

            // ###### 2、根据用户Id获取用户拥有的角色名称集合和权限code集合  并存入Session 中 end #######

            // 创建自己定义的LocalToken
            JwtToken jwtToken = new JwtToken(token);

            subject.login(jwtToken);


            // ###### 填充上下文信息 #############
            ContextParam contextParam = CommonContext.get();
            contextParam.setUserId(sysUser.getId());
            contextParam.setLoginName(sysUser.getLoginName());
            contextParam.setUserInfo(sessionUser);
            // ###### 填充上下文信息 #############

            log.info("用户【{0}】登录成功", loginName);

            return onLoginSuccess(jwtToken,subject,servletRequest,servletResponse);
        }catch (AccountNotFoundException | AccountPasswordException | AccountDisabledException | AccountInActiveException ex){
            // 登录失败
            log.error(ex, "用户登录失败:{0},{1}", loginName, ex.getMessage());
            return onLoginFailure(null, new AuthenticationException(ex), servletRequest, servletResponse);
        }
    }

    /**
     * 保存用户登录信息到Redis
     * @param currentTimeMillis 当前时间戳
     * @param loginName 登录账号
     * @param sessionUser 当前登录用户信息
     */
    protected void saveToRedis(String currentTimeMillis,String loginName,SessionUser sessionUser){
        // 保存Token刷新时间
        tokenManagerShiro.saveRefreshToken(currentTimeMillis,loginName,refreshTokenExpireTime);
        // 保存用户信息
        tokenManagerShiro.saveCurrentUser(loginName,sessionUser);
    }

    /**
     * 登录成功
     * @param token
     * @param subject
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;


        LoginResult loginResult = new LoginResult();
        loginResult.setSuccess(true);
        loginResult.setOpenCaptcha(false);
        loginResult.setUserName(getUsername(request));
        loginResult.setToken(token.getCredentials().toString());

//        // 移除登录出错次数
//        SessionManagerShiro.removeAttribute(S_LOGIN_ERROR_TIMES);
//        // 移除验证码值
//        SessionManagerShiro.removeAttribute(S_CAPTCHA_VALUE);
//        // 是否开启验证码 关闭
//        SessionManagerShiro.setAttribute(S_IS_OPEN_CAPTCHA,Boolean.FALSE);

        outPutLoginResult(response, loginResult);
        // 不再执行过滤器链后面的操作
        return false;
    }

    /**
     * 登录失败
     * @param token
     * @param ae
     * @param servletRequest
     * @param servletResponse
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest servletRequest, ServletResponse servletResponse) {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String message = "";
        Throwable e = ae.getCause();
        if (e != null && e instanceof LoginException) {

            if(e instanceof AccountPasswordException){
                AccountPasswordException exception = ((AccountPasswordException) e);

                // 如果是密码错误异常
                Integer loginErrorTimes = SessionManagerShiro.getAttribute(S_LOGIN_ERROR_TIMES);
                if(null == loginErrorTimes){
                    loginErrorTimes = 0;
                }

                loginErrorTimes = loginErrorTimes +1;

                // 剩余次数
                int remainTime = AUTHENTICATION_FAILURE_FREEZE_LIMIT - loginErrorTimes;

                if(remainTime>0 && remainTime< 5){
                    message = MessageFormat.format("密码错误,您还可以输入【{0}】次", remainTime);
                }else if(remainTime == 0){
                    message = MessageFormat.format("因于您的登录密码输入错误次数达到【{0}】次，账号已被系统临时冻结【{1}】小时!",AUTHENTICATION_FAILURE_FREEZE_LIMIT,AUTHENTICATION_FAILURE_FREEZE_HOUR);

                    // ###### 冻结系统用户 ########
                    SysUserFreezeModel sysUserFreezeModel = new SysUserFreezeModel();
                    sysUserFreezeModel.setId(exception.getUserId());
                    sysUserFreezeModel.setStatus(SysUserStatusEnum.DISABLED.getCode());
                    Date now = new Date();
                    sysUserFreezeModel.setFreezeTime(now);
                    sysUserFreezeModel.setFreezeStartTime(now);
                    sysUserFreezeModel.setFreezeEndTime(DateTool.addHours(now,AUTHENTICATION_FAILURE_FREEZE_HOUR));
                    sysUserFreezeModel.setFreezeContent(message);

                    BaseResult<Boolean> sysUserFreezeResult = ServiceUserTool.passportService().updateSysUserToFreeze(sysUserFreezeModel);
                    if(!sysUserFreezeResult.isSuccess()){
                        log.error("冻结系统用户【{0}】失败",getUsername(request));
                    }
                    // ###### 冻结系统用户 ########
                }

                SessionManagerShiro.setAttribute(S_LOGIN_ERROR_TIMES, loginErrorTimes);
            }else if (e instanceof AccountNotFoundException){
                message = e.getMessage();
                log.warn(e.getMessage());
            }else if (e instanceof AccountDisabledException){
                message = e.getMessage();
                log.warn(e.getMessage());
            }else if (e instanceof AccountInActiveException){
                message = e.getMessage();
                log.warn(e.getMessage());
            }
        }


        LoginResult loginResult = new LoginResult();
        loginResult.setSuccess(false);
        loginResult.setOpenCaptcha(true);
        loginResult.setUserName(getUsername(request));
        loginResult.setPassWord(getPassword(request));
        loginResult.setMessage(message);

        outPutLoginResult(response, loginResult);
        return false;
    }

    /**
     * 重写跳转到登录页
     * 因为是前后端分离，所以返回状态码给前端，前端自己根据状态码判断进行跳转
     * @param servletRequest
     * @param servletResponse
     * @throws IOException
     */
    @Override
    protected void redirectToLogin(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 返回状态码，交给前端去跳转
        response.setStatus(600);
        response.setHeader("headerStatus", "600");
        response.getWriter().close();
    }

    /**
     * 输出登录结果信息
     * @param response response
     * @param loginResult 登录结果
     */
    private void outPutLoginResult(HttpServletResponse response, LoginResult loginResult) {
        try {
            boolean success = loginResult.isSuccess();
            if(!success){
                // 失败
                // ajax不回传密码
                loginResult.setPassWord(null);
            }
            String result = JSON.toJSONString(loginResult);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=UTF-8");
            response.getWriter().write(result);
            response.getWriter().flush();
            response.getWriter().close();
        }catch (Exception ex){
            if (loginResult.isSuccess()) {
                log.error(ex, "登录成功,输出结果出现异常!");
            } else {
                log.error(ex, "登录失败,输出结果出现异常!");
            }
        }
    }


    /**
     * 获取用户输入的验证码
     * @param request
     * @return
     */
    private String getCaptch(ServletRequest request) {
        return WebUtils.getCleanParam(request, CAPTCHA_PARAM);
    }

    /**
     * 获取用户输入的动态码
     * @param request
     * @return
     */
    private String getAuthentication(ServletRequest request) {
        return WebUtils.getCleanParam(request, AUTHENTICATION_PARAM);
    }

    /**
     * 设置用户Session对象
     * @param sysUser
     * @return SessionUser
     */
    public SessionUser setSessionUser(SysUser sysUser){
        SessionUser sessionUser = new SessionUser();
        BeanTool.copyProperties(sysUser,sessionUser);
        sessionUser.setLoginTime(new Date());
        return sessionUser;
    }
}
